/*
 * Copyright © 2004-2014 chenYuan. All rights reserved.
 * @Website:wwww.jspx.net
 * @Mail:39793751@qq.com
 * @author: chenYuan , 陈原
 * @License: Jspx.net Framework Code is open source (LGPL)，Jspx.net Framework 使用LGPL 开源授权协议发布。
 * @jvm:jdk1.6+  x86/amd64
 *
 */
// Copyright (C) 1998-2001 by Jason Hunter <jhunter_AT_acm_DOT_org>.
// All rights reserved.  Use of this class is limited.
// Please see the LICENSE for more information.

package com.jspx.upload.multipart;

import com.jspx.boot.environment.Environment;
import com.jspx.utils.StringUtil;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Vector;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.ServletInputStream;

/**
 * A utility class to handle <code>multipart/form-data</code> requests,
 * the kind of requests that support file uploads.  This class uses a
 * "pull" model where the reading of incoming files and parameters is
 * controlled by the client code, which allows incoming files to be stored
 * into any <code>OutputStream</code>.  If you wish to use an API which
 * resembles <code>HttpServletRequest</code>, use the "push" model
 * <code>MultipartRequest</code> instead.  It's an easy-to-use wrapper
 * around this class.
 * <p/>
 * This class can receive arbitrarily large files (up to an artificial limit
 * you can set), and fairly efficiently too.
 * It cannot handle nested data (multipart content within multipart content).
 * It <b>can</b> now with the latest release handle internationalized content
 * (such as non Latin-1 filenames).
 * <p/>
 * It also optionally includes enhanced buffering and Content-Length
 * limitation.  Buffering is only required if your upload container is
 * poorly implemented (many are, including Tomcat 3.2),
 * but it is generally recommended because it will make a slow upload
 * container a lot faster, and will only make a fast upload container a
 * little slower.  Content-Length limiting is usually only required if you find
 * that your upload is hanging trying to read the input stram from the POST,
 * and it is similarly recommended because it only has a minimal impact on
 * performance.
 * <p/>
 * See the included upload.war for an example of how to use this class.
 * <p/>
 * The full file upload specification is contained in experimental RFC 1867,
 * available at <a href="http://www.ietf.org/rfc/rfc1867.txt">
 * http://www.ietf.org/rfc/rfc1867.txt</a>.
 *
 * @author Jason Hunter
 * @author Geoff Soutter
 * @version 1.0, 2000/10/27, initial revision
 * @see com.jspx.upload.MultipartRequest
 */
public class MultipartParser
{

    /**
     * input stream to read parts from
     */
    private ServletInputStream in;

    /**
     * MIME boundary that delimits parts
     */
    private String boundary;

    /**
     * reference to the last file part we returned
     */
    private FilePart lastFilePart;

    /**
     * buffer for readLine method
     */
    private byte[] buf = new byte[8 * 1024];
    /**
     * preferred encoding
     */
    private String encoding = Environment.defaultEncode;

    /**
     * Creates a <code>MultipartParser</code> from the specified request,
     * which limits the upload size to the specified length, buffers for
     * performance and prevent attempts to read past the amount specified
     * by the Content-Length.
     *
     * @param req     the upload request.
     * @param maxSize the maximum size of the POST content.
     */
    public MultipartParser(HttpServletRequest req,
                           int maxSize) throws IOException
    {
        this(req, maxSize, true, true);
    }

    /**
     * Creates a <code>MultipartParser</code> from the specified request,
     * which limits the upload size to the specified length, and optionally
     * buffers for performance and prevents attempts to read past the amount
     * specified by the Content-Length.
     *
     * @param req         the upload request.
     * @param maxSize     the maximum size of the POST content.
     * @param buffer      whether to do internal buffering or let the server buffer,
     *                    useful for servers that don't buffer
     * @param limitLength boolean flag to indicate if we need to filter
     *                    the request's input stream to prevent trying to
     *                    read past the end of the stream.
     */
    public MultipartParser(HttpServletRequest req, int maxSize, boolean buffer,
                           boolean limitLength) throws IOException
    {
        this(req, maxSize, buffer, limitLength, null);
    }

    /**
     * Creates a <code>MultipartParser</code> from the specified request,
     * which limits the upload size to the specified length, and optionally
     * buffers for performance and prevents attempts to read past the amount
     * specified by the Content-Length, and with a specified encoding.
     *
     * @param req         the upload request.
     * @param maxSize     the maximum size of the POST content.
     * @param buffer      whether to do internal buffering or let the server buffer,
     *                    useful for servers that don't buffer
     * @param limitLength boolean flag to indicate if we need to filter
     *                    the request's input stream to prevent trying to
     *                    read past the end of the stream.
     * @param encoding    the encoding to use for parsing, default is ISO-8859-1.
     */
    public MultipartParser(HttpServletRequest req, long maxSize, boolean buffer,
                           boolean limitLength, String encoding)
            throws IOException
    {
        // First make sure we know the encoding to handle chars correctly.
        // Thanks to Andreas Granzer, andreas.granzer@wave-solutions.com,
        // for pointing out the need to have this in the constructor.
        if (encoding != null)
        {
            setEncoding(encoding);
        }
        // Check the content type to make sure it's "multipart/form-data"
        // Access header two ways to work around WebSphere oddities
        String type = null;
        String type1 = req.getHeader("Content-Type");
        String type2 = req.getContentType();
        // If one value is null, choose the other value
        if (type1 == null && type2 != null)
        {
            type = type2;
        } else if (type2 == null && type1 != null)
        {
            type = type1;
        }
        // If neither value is null, choose the longer value
        else if (type1 != null)
        {
            type = (type1.length() > type2.length() ? type1 : type2);
        }

        if (type == null || !type.toLowerCase().startsWith("multipart"))
        {
            throw new IOException("Posted content type isn't multipart/form-data");
        }

        int length = req.getContentLength();
        if (maxSize!=0&&length > maxSize)
        {
            throw new ExceededSizeException("Posted content length of " + length + " exceeds limit of " + maxSize);
        }

        // Get the boundary string; it's included in the content type.
        String boundary = extractBoundary(type);
        if (boundary == null)
        {
            throw new IOException("Separation boundary was not specified");
        }

        ServletInputStream in = req.getInputStream();

        // If required, wrap the real input stream with classes that
        // "enhance" its behaviour for performance and stability
        if (buffer)
        {
            in = new BufferedServletInputStream(in);
        }
        if (limitLength)
        {
            // Check the content length to prevent denial of components attacks
            in = new LimitedServletInputStream(in, length);
        }

        // Save our values for later
        this.in = in;
        this.boundary = boundary;

        // Read until we hit the boundary
        // Some clients send a preamble (per RFC 2046), so ignore that
        // Thanks to Ben Johnson, ben.johnson@merrillcorp.com, for pointing out
        // the need for preamble support.
        do
        {
            String line = readLine();
            if (line == null)
            {
                throw new IOException("Corrupt form data: premature ending");
            }
            // See if this line is the boundary, and if so break
            if (line.startsWith(boundary))
            {
                break;  // success
            }
        } while (true);
    }

    /**
     * Sets the encoding used to parse from here onward.  The default is
     * ISO-8859-1.  Encodings are actually best passed into the contructor,
     * so even the initial line reads are correct.
     *
     * @param encoding The encoding to use for parsing
     */
    public void setEncoding(String encoding)
    {
        this.encoding = encoding;
    }

    /**
     * Read the next part arriving in the stream. Will be either a
     * <code>FilePart</code> or a <code>ParamPart</code>, or <code>null</code>
     * to indicate there are no more parts to read. The order of arrival
     * corresponds to the order of the form elements in the submitted form.
     *
     * @return either a <code>FilePart</code>, a <code>ParamPart</code> or
     *         <code>null</code> if there are no more parts to read.
     * @throws IOException if an input or output exception has occurred.
     * @see FilePart
     * @see ParamPart
     */
    public Part readNextPart() throws IOException
    {
        // Make sure the last file was entirely read from the input
        if (lastFilePart != null)
        {
            lastFilePart.getInputStream().close();
            lastFilePart = null;
        }

        // Read the headers; they look like this (not all may be present):
        // Content-Disposition: form-data; name="field1"; filename="file1.txt"
        // Content-Type: type/subtype
        // Content-Transfer-Encoding: binary
        Vector<String> headers = new Vector<String>();

        String line = readLine();
        if (line == null)
        {
            // No parts left, we're done
            return null;
        } else if (line.length() == 0)
        {
            // IE4 on Mac sends an empty line at the end; treat that as the end.
            // Thanks to Daniel Lemire and Henri Tourigny for this fix.
            return null;
        }

        // Read the following header lines we hit an empty line
        // A line starting with whitespace is considered a continuation;
        // that requires a little special logic.  Thanks to Nic Ferrier for
        // identifying a good fix.
        while (line != null && line.length() > 0)
        {
            String nextLine = null;
            boolean getNextLine = true;
            while (getNextLine)
            {
                nextLine = readLine();
                if (nextLine != null
                        && (nextLine.startsWith(" ")
                        || nextLine.startsWith("\t")))
                {
                    line = line + nextLine;
                } else
                {
                    getNextLine = false;
                }
            }
            // Add the line to the header list
            headers.addElement(line);
            line = nextLine;
        }

        // If we got a null above, it's the end
        if (line == null)
        {
            return null;
        }

        String name = null;
        String filename = null;
        String origname = null;
        String contentType = "text/plain";  // rfc1867 says this is the default

        Enumeration enums = headers.elements();
        while (enums.hasMoreElements())
        {
            String headerline = (String) enums.nextElement();
            if (headerline.toLowerCase().startsWith("content-disposition:"))
            {
                // Parse the content-disposition line
                String[] dispInfo = extractDispositionInfo(headerline);
                // String disposition = dispInfo[0];  // not currently used
                name = dispInfo[1];
                filename = dispInfo[2];
                origname = dispInfo[3];
            } else if (headerline.toLowerCase().startsWith("content-type:"))
            {
                // Get the content type, or null if none specified
                String type = extractContentType(headerline);
                if (!StringUtil.isNULL(type))
                {
                    contentType = type;
                }
            }
        }

        // Now, finally, we read the content (end after reading the boundary)
        if (filename == null)
        {
            // This is a parameter, add it to the vector of values
            // The encoding is needed to help parse the value
            return new ParamPart(name, in, boundary, encoding);
        } else
        {
            // This is a file
            if (filename.equals(""))
            {
                filename = null; // empty filename, probably an "empty" file param
            }
            lastFilePart = new FilePart(name, in, boundary,contentType, filename, origname);
            return lastFilePart;
        }
    }

    /**
     * Extracts and returns the boundary token from a line.
     *
     * @return the boundary token.
     */
    private String extractBoundary(String line)
    {
        // Use lastIndexOf() because IE 4.01 on Win98 has been known to send the
        // "boundary=" string multiple times.  Thanks to David Wall for this fix.
        int index = line.lastIndexOf("boundary=");
        if (index == -1)
        {
            return null;
        }
        String boundary = line.substring(index + 9);  // 9 for "boundary="
        if (boundary.charAt(0) == '"')
        {
            // The boundary is enclosed in quotes, strip them
            index = boundary.lastIndexOf('"');
            boundary = boundary.substring(1, index);
        }

        // The real boundary is always preceeded by an extra "--"
        boundary = "--" + boundary;

        return boundary;
    }

    /**
     * Extracts and returns disposition info from a line, as a <code>String<code>
     * array with elements: disposition, name, filename.
     *
     * @return String[] of elements: disposition, name, filename.
     * @throws IOException if the line is malformatted.
     */
    private String[] extractDispositionInfo(String line) throws IOException
    {
        // Return the line's data as an array: disposition, name, filename
        String[] retval = new String[4];

        // Convert the line to a lowercase string without the ending \r\n
        // Keep the original line for error messages and for variable names.
        String origline = line;
        line = origline.toLowerCase();

        // Get the content disposition, should be "form-data"
        int start = line.indexOf("content-disposition: ");
        int end = line.indexOf(StringUtil.SEMICOLON);
        if (start == -1 || end == -1)
        {
            throw new IOException("Content disposition corrupt: " + origline);
        }
        String disposition = line.substring(start + 21, end);
        if (!disposition.equals("form-data"))
        {
            throw new IOException("Invalid content disposition: " + disposition);
        }

        // Get the field name
        start = line.indexOf("name=\"", end);  // start at last semicolon
        end = line.indexOf("\"", start + 7);   // skip name=\"
        int startOffset = 6;
        if (start == -1 || end == -1)
        {
            // Some browsers like lynx don't surround with ""
            // Thanks to Deon van der Merwe, dvdm@truteq.co.za, for noticing
            start = line.indexOf("name=", end);
            end = line.indexOf(StringUtil.SEMICOLON, start + 6);
            if (start == -1)
            {
                throw new IOException("Content disposition corrupt: " + origline);
            } else if (end == -1)
            {
                end = line.length();
            }
            startOffset = 5;  // without quotes we have one fewer char to skip
        }
        String name = origline.substring(start + startOffset, end);

        // Get the filename, if given
        String filename = null;
        String origname = null;
        start = line.indexOf("filename=\"", end + 2);  // start after name
        end = line.indexOf("\"", start + 10);          // skip filename=\"
        if (start != -1 && end != -1)
        {                // note the !=
            filename = origline.substring(start + 10, end);
            origname = filename;
            // The filename may contain a full path.  Cut to just the filename.
            int slash =
                    Math.max(filename.lastIndexOf('/'), filename.lastIndexOf('\\'));
            if (slash > -1)
            {
                filename = filename.substring(slash + 1);  // past last slash
            }
        }

        // Return a String array: disposition, name, filename
        // empty filename denotes no file posted!
        retval[0] = disposition;
        retval[1] = name;
        retval[2] = filename;
        retval[3] = origname;
        return retval;
    }

    /**
     * Extracts and returns the content type from a line, or null if the
     * line was empty.
     *
     * @return content type, or null if line was empty.
     * @throws IOException if the line is malformatted.
     */
    private static String extractContentType(String line) throws IOException
    {
        // Convert the line to a lowercase string
        line = line.toLowerCase();

        // Get the content type, if any
        // Note that Opera at least puts extra info after the type, so handle
        // that.  For example:  Content-Type: text/plain; name="foo"
        // Thanks to Leon Poyyayil, leon.poyyayil@trivadis.com, for noticing this.
        int end = line.indexOf(com.jspx.utils.StringUtil.SEMICOLON);
        if (end == -1)
        {
            end = line.length();
        }

        return line.substring(13, end).trim();  // "content-type:" is 13
    }

    /**
     * Read the next line of input.
     *
     * @return a String containing the next line of input from the stream,
     *         or null to indicate the end of the stream.
     * @throws IOException if an input or output exception has occurred.
     */
    private String readLine() throws IOException
    {
        StringBuilder buffer = new StringBuilder();
        int result;

        do
        {
            result = in.readLine(buf, 0, buf.length);  // does +=
            if (result != -1)
            {
                buffer.append(new String(buf, 0, result, encoding));
            }
        } while (result == buf.length);  // loop only if the buffer was filled

        if (buffer.length() == 0)
        {
            return null;  // nothing read, must be at the end of stream
        }

        // Cut off the trailing \n or \r\n
        // It should always be \r\n but IE5 sometimes does just \n
        // Thanks to Luke Blaikie for helping make this work with \n
        int len = buffer.length();
        if (len >= 2 && buffer.charAt(len - 2) == '\r')
        {
            buffer.setLength(len - 2);  // cut \r\n
        } else if (len >= 1 && buffer.charAt(len - 1) == '\n')
        {
            buffer.setLength(len - 1);  // cut \n
        }

        return buffer.toString();
    }
}